package main

import (
	"flag"
	"os"
	"sort"
	"strconv"
	"strings"

	"gitee.com/bon-ami/eztools/v4"
)

const (
	// keep these top in a const group to begin from 0
	fldsIndNum = iota
	fldsIndName
	fldsIndNick
	fldsIndTeam

	module = "contacts"

	// for tblContacts
	tblCONTACTS = "TblContacts"
	// for tblTeam
	tblTEAM = "TblTeam"
	// for fldLeader
	fldLEADER = "FldLeader"
	// for addresses
	fldADDR     = "FldAddr"
	fldLAT      = "FldLatitude"
	fldLNG      = "FldLongitude"
	fldPROVINCE = "FldProvince"
	// for fldsStrs[fldsInd*]
	fldNUM  = "FldNumber"
	fldNAME = "FldName"
	fldNICK = "fldNick"
	fldTEAM = "fldTeam"
)

var (
	Ver, Bld string
	Silent   bool
	tblDef, tblContacts, tblTeam,
	fldId, fldStr, fldLeader, fldAddr, fldLat, fldLng, fldProvince string
	// fldsStrs contains all fields by design/in db, except id
	fldsStrs []string
	// fldsStrAs1 joins fldsStrs
	fldsStrAs1 string
	// key fields are unique, to distinguish among records, when importing. not ordered
	keyFlds = []int{fldsIndNum, fldsIndName, fldsIndNick}
	// must exist fields. not ordered
	mustFlds = []int{fldsIndName, fldsIndTeam}
	// generated fields not to be input by user. not ordered
	genFlds []string
	// index for addresses, positions may not be valid
	// fldsIndLat and fldsIndLng are indexes in genFlds
	fldsIndAddr, fldsIndLat, fldsIndLng int
)

func initFieldNames(db eztools.Dbs) (ret string, err error) {
	fldId = db.GetFldID()
	fldStr = db.GetFldStr()
	tblDef = db.GetTblDef()
	tblContacts, err = db.GetPair(tblDef, tblCONTACTS, fldId, fldStr)
	if err != nil {
		return "NO contact table defined!", err
	}
	tblTeam, err = db.GetPair(tblDef, tblTEAM, fldId, fldStr)
	if err != nil {
		return "NO team table defined!", err
	}
	fldLeader, err = db.GetPair(tblDef, fldLEADER, fldId, fldStr)
	if err != nil {
		eztools.LogPrint("NO leader field defined!", err)
		err = nil
	}
	fldAddr, _ = db.GetPair(tblDef, fldADDR, fldId, fldStr)
	fldLat, _ = db.GetPair(tblDef, fldLAT, fldId, fldStr)
	fldLng, _ = db.GetPair(tblDef, fldLNG, fldId, fldStr)
	fldProvince, _ = db.GetPair(tblDef, fldPROVINCE, fldId, fldStr)
	genFlds = append(genFlds, fldId)
	if len(fldLat) > 0 && len(fldLng) > 0 {
		fldsIndLat = len(genFlds)
		fldsIndLng = fldsIndLat + 1
		genFlds = append(genFlds, []string{fldLat, fldLng, fldProvince}...)
	} else {
		fldsIndLat = eztools.InvalidID
		fldsIndLng = eztools.InvalidID
		if len(fldProvince) > 0 {
			// a strange situation. what is this field for?
			genFlds = append(genFlds, fldProvince)
		}
	}

	_, allFlds, err := db.Describe(tblContacts)
	if err != nil {
		return "FAILED to list table fields!", err
	}
	// make sure following order match fldsInd*
	// and keyFlds are in the beginning
	// and fldTeam is next, for CheckContacts()
	// these fields are static fields of tblContacts
	flds := []string{fldNUM, fldNAME, fldNICK, fldTEAM}
	for i := 0; i < len(flds); i++ {
		str, err := db.GetPair(tblDef, flds[i], fldId, fldStr)
		fldsStrs = append(fldsStrs, str)
		if err != nil {
			eztools.LogPrint(flds[i]+
				" NOT defined in table "+
				tblDef, err)
		}
	}
	// now the dynamic fields of tblContacts
	sttFlds := make([]string, len(fldsStrs))
	copy(sttFlds, fldsStrs)
	sort.Strings(sttFlds)
	sort.Strings(allFlds)
	indFlds := 0
	lenFlds := len(sttFlds)
	fldsIndAddr = eztools.InvalidID
LoopAllFlds:
	for _, i := range allFlds {
		if indFlds < lenFlds {
			// static?
			//eztools.ShowStrln("comparing", sttFlds[indFlds], i)
			if sttFlds[indFlds] == i {
				indFlds++
				continue
			}
			// static fields must all exist
		}
		// generated fields are excluded from fldsStrs
		for _, gen := range genFlds {
			if i == gen {
				continue LoopAllFlds
			}
		}
		if i == fldAddr {
			fldsIndAddr = len(fldsStrs)
		}
		fldsStrs = append(fldsStrs, i)
		//eztools.ShowStrln(i, fldsStrs)
	}

	fldsStrAs1 = strings.Join(fldsStrs, ", ")
	return
}

func main() {
	var (
		paramVersion, paramH, paramHelp, paramV, paramVV, paramVVV bool
		paramLog, paramImp, paramExp, paramEnc,
		paramCmd, paramId, paramI2, paramAddr, paramFile, paramExt string
	)
	flag.BoolVar(&paramVersion, "version", false, "help info")
	flag.BoolVar(&paramH, "h", false, "help info")
	flag.BoolVar(&paramHelp, "help", false, "help info")
	flag.BoolVar(&paramV, "v", false, "some log")
	flag.BoolVar(&paramVV, "vv", false, "verbose messages")
	flag.BoolVar(&paramVVV, "vvv", false,
		"maybe more verbose messages")
	flag.StringVar(&paramLog, "log", "", "log file")
	flag.StringVar(&paramImp, "imp", "", "input csv file to parse as contact info to change. Without adequate -enc param, there will be issues for some characters. comma separated. first line as field names.")
	flag.StringVar(&paramExp, "exp", "", "export csv file, with all fields, or with optional current file containing some fields. Without adequate -enc param, there will be issues for some characters. comma separated. first line as field names.")
	flag.StringVar(&paramEnc, "enc", "", "encoding for -f, default is UTF-8. available values include: "+strings.Join(eztools.ListEnc(), ", "))
	flag.StringVar(&paramCmd, "cmd", "", "command to run in silent mode")
	flag.StringVar(&paramId, "id", "", "ID to use for some commands")
	flag.StringVar(&paramI2, "i2", "", "other ID to use for some commands")
	flag.StringVar(&paramAddr, "addr", "", "address to use for some commands")
	flag.StringVar(&paramFile, "file", "", "file to use for some commands")
	flag.StringVar(&paramExt, "ext", "", "file extension to use for some commands")
	flag.Parse()
	if len(Ver) < 1 {
		Ver = "dev"
	}
	const promptPref = ":::"
	if paramVersion || paramH || paramHelp {
		eztools.ShowStrln("version " + Ver + " build " + Bld)
		flag.Usage()
		eztools.ShowStrln(promptPref, "Commands")
		eztools.ShowStrln("adduser/useradd: add a contact")
		eztools.ShowStrln("usermod: modify a contact. -id may be used together.")
		eztools.ShowStrln("deluser/userdel: delete a contact. -id to be used together, to be silent.")
		eztools.ShowStrln("groupmems: assign a team leader. -id for team and -i2 for contact, to be used together, to be silent.")
		eztools.ShowStrln("groupmod: rename a team. -id may be used together.")
		eztools.ShowStrln("delgroup/groupdel: delete a team. -id to be used together, to be silent.")
		eztools.ShowStrln("parsehomes/homesparse: parse contacts' addresses")
		eztools.ShowStrln("checkriskdist: check contacts' distance to an address. -addr to be used together, to be silent.")
		eztools.ShowStrln("staticmapanony: generate static map without user names. -file to be used together, to be saved to, which defaults to under current directory.")
		eztools.ShowStrln("staticmapnames: generate static map with user names. -file to be used together, to be saved to, which defaults to under current directory.")
		return
	}
	eztools.Debugging = paramV || paramVV || paramVVV
	switch {
	case paramV:
		eztools.Verbose = 1
	case paramVV:
		eztools.Verbose = 2
	case paramVVV:
		eztools.Verbose = 3
	}
	if eztools.Debugging {
		if len(paramLog) < 1 {
			paramLog = module + ".log"
		}
	}
	if len(paramLog) > 0 {
		logger, err := os.OpenFile(paramLog,
			os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
		if err == nil {
			if err = eztools.InitLogger(logger); err != nil {
				eztools.LogPrint(err)
			}
		} else {
			eztools.LogPrint("Failed to open log file "+paramLog, err)
		}
	}
	db, cfg, err := eztools.MakeDbs()

	if err != nil {
		eztools.LogFatal(err)
	}
	defer func() {
		db.Close()
		switch err {
		case nil:
			return
		default:
			os.Exit(1)
		}
	}()

	upch := make(chan bool, 2)
	go db.AppUpgrade("", module, Ver, nil, upch)

	inf, err := initFieldNames(*db)
	if err != nil {
		eztools.LogPrint(inf)
		eztools.LogFatal(err)
	}
	switch {
	case len(paramImp) > 0:
		err = importCSV(db, paramImp, paramEnc)
		return
	case len(paramExp) > 0:
		err = exportCSV(db, paramExp, paramEnc)
		return
	}
	choices := []string{"Exit", //0
		"Add a contact",        //1
		"Modify a contact",     //2
		"Delete a contact",     //3
		"Assign a team leader", //4
		"Rename a team",        //5
		"Delete a team",        //6
	}
	if fldsIndAddr != eztools.InvalidID &&
		fldsIndLat != eztools.InvalidID &&
		fldsIndLng != eztools.InvalidID &&
		len(cfg.Map) > 0 {
		choices = append(choices,
			"Parse contacts' addresses",              //7
			"Check contacts' distance to an address", //8
			"Generate static maps (anonymously)",     //9
			"Generate static maps (with names)",      //10
		)
	}
LOOP:
	for {
		var ans int
		if len(paramCmd) > 0 {
			switch paramCmd {
			case "adduser", "useradd":
				ans = 1
			case "usermod":
				ans = 2
			case "deluser", "userdel":
				ans = 3
			case "groupmems":
				ans = 4
			case "groupmod":
				ans = 5
			case "delgroup", "groupdel":
				ans = 6
			case "parsehomes", "homesparse":
				ans = 7
			case "checkriskdist":
				ans = 8
			case "staticmapanony":
				ans = 9
			case "staticmapnames":
				ans = 10
			}
		} else {
			notification := ""
			if err, detail := CheckContacts(db); err != nil {
				eztools.LogPrint(err)
				switch err {
				case eztools.ErrOutOfBound,
					eztools.ErrNoValidResults,
					eztools.ErrIncomplete:
					notification = "Current database needs maintenance!" + detail
				}
			}
			eztools.ShowStrln(notification)
			eztools.ShowStrln(promptPref, "Current contacts,")
			if err = ListContacts(db, eztools.InvalidID); err != nil {
				eztools.LogFatal(err)
			}
			eztools.ShowStrln(promptPref, "Current teams,")
			if err = ListTeams(db); err != nil {
				eztools.LogFatal(err)
			}
			eztools.ShowStrln(notification)
			eztools.ShowStrln(promptPref, "Operations,")
			ans, _ = eztools.ChooseStrings(choices)
		}
		err = nil
		id := paramId
		i2 := paramI2
		addr := paramAddr
		switch ans {
		case 0, eztools.InvalidID:
			break LOOP
		case 1:
			AddContact(db)
		case 2, 3:
			if len(id) < 1 {
				id = eztools.PromptStr("ID of the contact")
			}
			if len(id) < 1 {
				break
			}
			switch ans {
			case 2:
				err = ModifyContact(db, id)
			case 3:
				err = DeleteContact(db, id)
			}
		case 4, 5, 6:
			if len(id) < 1 {
				id = eztools.PromptStr("ID of the team")
			}
			if len(id) < 1 {
				break
			}
			switch ans {
			case 4:
				err = MakeLeader(db, i2, id)
			case 5:
				err = ModifyTeam(db, id)
			case 6:
				err = DeleteTeam(db, id)
			}
		case 7:
			err = Address2Position4Contacts(db, cfg.Map)
		case 8:
			if len(addr) < 1 {
				addr = eztools.PromptStr("Address to check")
			}
			if len(addr) > 0 {
				const distance = 5000
				err = Distance2Position4Contacts(db, cfg.Map, addr, distance)
			}
		case 9, 10:
			var anony string
			switch ans {
			case 9:
				anony = "anony"
			case 10:
				anony = "named"
			}
			fnBase := paramFile
			if len(fnBase) < 1 {
				fnBase = "map-" + anony + "-"
			}
			fnExt := paramExt
			if len(fnExt) < 1 {
				fnExt = ".png"
			}
			err = GenSttMap(db, cfg.Map, anony, fnBase, fnExt)
		}
		if err != nil {
			eztools.LogPrint(err)
		}
		if len(paramCmd) > 0 {
			break
		}
	}
	if !(<-upch) {
		eztools.LogPrint("wrong server for update check")
	} else {
		if !(<-upch) {
			eztools.LogPrint("update check failed")
		} else {
			if eztools.Debugging {
				eztools.LogPrint("update check done/skipped")
			}
		}
	}
}

func chkInputFileHeaders(keyFlds, indexes []int, def, in []string) (string, bool) {
	lenIn := len(in)
	stIn := make([]string, lenIn)
	copy(stIn, in)
	sort.Strings(stIn)

	lenDef := len(def)
	stDef := make([]string, lenDef)
	copy(stDef, def)
	sort.Strings(stDef)

	nf := ""
	ok := true
	var be string
	// two pointers to match between the slices
	for indDef, indIn := 0, 0; indDef < lenDef && indIn < lenIn; {
		switch {
		case stDef[indDef] == stIn[indIn]:
			indDef++
			indIn++
		case stDef[indDef] > stIn[indIn]:
			if ok {
				ok = false
				be = "is"
			} else {
				be = "are"
			}
			nf += stIn[indIn] + " "
			indIn++
		case stDef[indDef] < stIn[indIn]:
			indDef++
		}
	}
	if !ok {
		return nf + be + " unrecognized!", false
	}

	for i := range indexes {
		indexes[i] = eztools.InvalidID
	}
	keyFound := false
	/*eztools.LogPrint("fldstrs", fldsStrs)
	eztools.LogPrint("in", in)*/
	for _, v := range in {
		for i, keyIND := range keyFlds {
			if v == fldsStrs[keyIND] {
				indexes[keyIND] = i
				if eztools.Debugging && eztools.Verbose > 1 {
					eztools.ShowStrln(v, "indexes[", keyIND, "]=", i)
				}
				keyFound = true
				break
			}
		}
	}
	if !keyFound {
		return "NO key fields read.", false
	}
	return nf, ok
}

// constructOrWoEmpty makes a criterea of "fldsStrs[indexes[0]]=csv[0] OR ... OR ..."
//	fldsIndNick is also matched for csv[fldsIndName]
func constructOrWoEmpty(csv []string, indexes []int) (ret string) {
	//eztools.LogPrint(indexes)
	for _, i := range indexes {
		if i == eztools.InvalidID {
			continue
		}
		if len(csv[i]) < 1 {
			continue
		}
		//eztools.LogPrint(i, "===")
		if len(ret) > 0 {
			ret += " OR "
		}
		ret += fldsStrs[i] + "=\"" + csv[i] + "\""
		if i == fldsIndName {
			// match it to nick, too
			ret += " OR " + fldsStrs[fldsIndNick] +
				" LIKE \"%" + csv[i] + "%\""
		}
		//eztools.LogPrint(fldsStrs[i], ":", ret, "===")
	}
	//eztools.LogPrint("final ", ret, "===")
	return
}

func readCSV(paramF, paramEnc string) (csv [][]string, err error) {
	if len(paramEnc) > 0 {
		csv, err = eztools.CSVReadWtEnc(paramF, eztools.EncodingGbk)
	} else {
		if !eztools.ChkCfmNPrompt("NO -enc param means UTF-8. Be sure to continue", "n") {
			eztools.LogFatal("no -enc param without -f")
		}
		csv, err = eztools.CSVRead(paramF)
	}
	if err != nil {
		eztools.LogPrint("FAILED to read "+paramF, err)
		return
	}
	if eztools.Debugging && eztools.Verbose > 1 {
		eztools.ShowSthln(csv)
	}
	return
}

func filterEmptyPairs(namI []string, valI []string) (namO []string, valO []string) {
	for i, _ := range namI {
		if len(namI[i]) > 0 && len(valI[i]) > 0 {
			namO = append(namO, namI[i])
			valO = append(valO, valI[i])
		}
	}
	return
}

// no action if any field is unrecognized.
// all fields override existing data, including empty value
func importCSV(db *eztools.Dbs, paramF, paramEnc string) (err error) {
	csv, err := readCSV(paramF, paramEnc)
	if err != nil {
		return
	}
	if len(csv) < 2 {
		err = eztools.ErrInvalidInput
		eztools.LogPrint("Both header and data needed for csv!")
		return
	}
	// header check
	indexes := make([]int, len(fldsStrs))
	// nick cannot be set as a key field, because it may contain multiple values
	nf, ok := chkInputFileHeaders(keyFlds, indexes, fldsStrs, csv[0])
	if !ok {
		err = eztools.ErrInvalidInput
		if eztools.Debugging {
			eztools.LogPrint("Read fields are", csv[0])
		}
		var keys []string
		for _, i := range keyFlds {
			keys = append(keys, fldsStrs[i])
		}
		eztools.LogPrint(nf+" Valid fields are", fldsStrAs1,
			"(but not ID). Key/Index fields are", strings.Join(keys, ", "))
		return
	}
	var existingDB [][]string
	for i := 1; i < len(csv); i++ {
		if eztools.Debugging {
			eztools.ShowStrln("Processing", csv[i])
		}
		// change current or add a new one?
		existingDB, err = db.Search(tblContacts,
			constructOrWoEmpty(csv[i], indexes),
			append(fldsStrs, fldId), "")
		//[]string{fldId, fldsStrs[fldsIndName],
		//fldsStrs[fldsIndNick]}, "")
		if err != nil {
			eztools.LogPrint(err)
			continue
		}
		existing := len(existingDB)
		if existing > 0 {
			eztools.ShowStrln("Found following existing contact(s)")
			eztools.ShowStrln(existingDB)
			up := eztools.PromptStr("Update it?([Enter]=y)")
			if len(up) > 0 && (up[0] == 'n' || up[0] == 'N') {
				existing = 0
			}
		}
		n, v := filterEmptyPairs(csv[0], csv[i])
		switch existing {
		case 0:
			_, err = db.AddWtParams(tblContacts,
				n, v, false)
		case 1:
			err = updateExistingContact(db, n, v,
				existingDB[0], indexes)
			if err == eztools.ErrNoValidResults {
				err = nil
			}
		default:
			eztools.LogPrint("line ", i,
				"matches multiple items in existing database!")
			err = eztools.ErrOutOfBound
		}
		switch err {
		case nil, eztools.ErrAbort, eztools.ErrNoValidResults, eztools.ErrOutOfBound:
			// do nothing
			break
		default:
			eztools.LogPrint("failed to change for line "+
				strconv.Itoa(i), err)
		}
	}
	return
}

func exportCSV(db *eztools.Dbs, paramF, paramEnc string) (err error) {
	var csv [][]string
	if _, err = os.Stat(paramF); err == nil {
		csv, err = readCSV(paramF, paramEnc)
		if err != nil {
			eztools.LogPrint("csv FAILED to be read. All fields will be output.")
		} else if csv == nil || len(csv) < 1 {
			eztools.LogPrint("Now header found in csv. All fields will be output.")
		}
	}
	var sel, flds []string
	_, flds, err = db.Describe(tblContacts)
	if err != nil {
		eztools.Log("FAILED to list fields of table", err)
	}
	eztools.Log("acceptable fields are", flds)
	if csv != nil && len(csv) > 0 {
		sel = csv[0]
		if eztools.Debugging && eztools.Verbose > 1 {
			eztools.LogPrint("to export", sel)
		}
	}
	data, err := db.Search(tblContacts, "", sel, "")
	if err != nil {
		eztools.LogPrint("NO contacts found in table", err)
		return
	}
	if sel == nil {
		sel = flds
	}
	data = append([][]string{sel}, data...)
	if eztools.Debugging && eztools.Verbose > 2 {
		eztools.ShowSthln(data)
	}
	if len(paramEnc) > 0 {
		err = eztools.CSVWriteWtEnc(paramF, eztools.EncodingGbk, data)
	} else {
		err = eztools.CSVWrite(paramF, data)
	}
	if err != nil {
		eztools.LogPrint("FAILED to write to "+paramF, err)
		return
	}
	return
}
